<?php
/**
 * Model driven code generator Class.
 * Driven by the <code>mdeol.xml</code> file, this class will generate all 
 * entities and their persistence classes into the app/entities path of the
 * framework if there are not already existing.
 * @author Frédéric Delorme<frederic.delorme@gmail.com>
 * @version 1.0 - 20091219
 */
class Model {
	/**
	 * Instance
	 * @var Model
	 */
	static private $_instance;
	/**
	 * DOM for the loaded <code>model.xml</code> file.
	 * @var array
	 */
	private $xml = array();
	/**
	 * All entities created and maintained by the <code>model.xml</code>.
	 * @var unknown_type
	 */
	private $entities = array();
	
	private $connection=null;
	
	/**
	 * default constructor.
	 */
	function __construct($connection) {
		if(!$this->xml){
			$this->xml = simplexml_load_file("config/model.xml");
			__debug(__FILE__,__LINE__,"Model::__construct() - model loaded.");
			$this->connection=$connection;
		}
	}
	
	private function init(){
		foreach($this->xml->entity as $entity){
			$name="".$entity->attributes()->name;
			$this->entities[$name] = $entity;
		}
	}
	/**
	 * Entities classes + SQL generator
	 * all php classes files for entities described into the <code>model.xml</code>
	 * file are generated into the <code>app/entities</code> directory.
	 * the SQL file maintaining the database model is generated into 
	 * <code>app/script/model.sql</code>.
	 * @param boolean $forced flag forcing the code generation and table creation. 
	 */
	public function generate($forced=false){
		__debug(__FILE__,__LINE__,"Model::generate() - start");
		$fullsql="";
		
		if($this->xml && count($this->xml->entity)){
			$this->init();
			$cpt=0;
			foreach($this->xml->entity as $entity){
				__debug(__FILE__,__LINE__,"Model::generate() - entity name=".$entity->attributes()->name);
				
				$sql="";$cpt++;
				$fent=dirname($_SERVER['SCRIPT_FILENAME'])."/app/entities/".$entity->attributes()->name.".class.php";
				if(!file_exists($fent) || $forced){
					$code = $this->createClass($entity);
					$sql .= $this->createSQL($entity);
					$this->serialize($entity,$code);
				__debug(__FILE__,__LINE__,"Model::generate() - entity name=".$entity->attributes()->name);
					$this->writeSQL($cpt."-".$entity->attributes()->name, $sql);
				__debug(__FILE__,__LINE__,"Model::generate() - entity name=".$entity->attributes()->name);
					$fullsql.=$sql;
				}
			}
			__debug(__FILE__,__LINE__,"Model::generate() - write SQL script");
			$sqlscript = $this->writeSQL("0-full", $fullsql);
			$this->connection->executeScript($sqlscript);
			__debug(__FILE__,__LINE__,"Model::generate() - create database tables: $sqlscript");
		}
		__debug(__FILE__,__LINE__,"Model::generate() - end");
	}
	
	/**
	 * TODO Implements SQL generation and serialization.
	 * Create SQL statements for <code>$entity</code> persistence schema.
	 * @param XMLElement $entity
	 * @return sql script.
	 */
	private function createSQL($entity){
		__debug(__FILE__,__LINE__,"Model::createSQL() - entity=".$entity->attributes()->name);
		$sql="-- entity ".$entity->attributes()->name."\r\n"; 
		$sql.="DROP TABLE ".$entity->persistence->attributes()->table.";\r\n";
		$sql.="CREATE TABLE ".$entity->persistence->attributes()->table." (";
		$fields=array();
		foreach($entity->field as $field){
			$define=$field->attributes()->name." ".
			$this->connection->decodeType($field->attributes()->type,
					(isset($field->attributes()->size)?$field->attributes()->size:""));
			if("".$field->attributes()->name == "".$entity->attributes()->primaryKey){
				$define.=" NOT NULL";
			}
			$fields["".$field->attributes()->name]=$define;
		}
		foreach($entity->relation as $relation){
			$define=$relation->attributes()->name." ";
			if("".$relation->attributes()->type=="many-to-one" ){
				//__debug(__FILE__,__LINE__,"Model::createSQL() - relation attributes=".print_r($relation->attributes(),true));
				$entity=array();
				$entities = $this->xml->xpath("entity[@name='".$relation->attributes()->entity."']");
				$entity=$entities[0];
				//__debug(__FILE__,__LINE__,"Model::createSQL() - relation to entity=".print_r($entity[0],true));
				$field=array();
				$fieldname=("".$relation->attributes()->relationKey);
				//__debug(__FILE__,__LINE__,"Model::createSQL() - relation to fieldname=".print_r($fieldname,true));
				$field = $this->xml->xpath("entity[@name='".$relation->attributes()->entity."']/field[@name='".("".$relation->attributes()->relationKey)."']");
				//__debug(__FILE__,__LINE__,"Model::createSQL() - relation to field=".print_r($field,true));
				$define.=$this->connection->decodeType($field[0]->attributes()->type,
									(isset($field[0]->attributes()->size)?$field[0]->attributes()->size:""));
				$fields["".$relation->attributes()->name]=$define;
			}
		}
		$sql.=implode(",",$fields);
		$sql.=", PRIMARY KEY (".$entity->attributes()->primaryKey.")";
		$sql.=");"."\r\n";
		__debug(__FILE__,__LINE__,"Model::createSQL() - end");		
		return $sql;
	}
	
	/**
	 * 
	 * @param array $entity
	 * @return text od generated code.
	 */
	private function createClass($entity){
		__debug(__FILE__,__LINE__,"Model::createClass() - entity=".$entity->attributes()->name);
		// Start class.
		$code = "<?php\r\n".
			   "require_once('modules/model/entity.class.php');\r\n".
			   "/**\r\n".
			   " * Generated class for ".$entity->attributes()->name."\r\n".
			   " * create on ".date("Y/m/d h:i")."\r\n".
			   " */\r\n".
			   "class ".ucwords($entity->attributes()->name)." extends Entity {\r\n";
		
		// generate constructor
		$code .= "  function __construct(\$connection=\"\"){\r\n";
		$code .= "    parent::__construct(\$connection=\"\");\r\n";
		$code .= "    \$this->table=\"".$entity->persistence->attributes()->table."\";\r\n";
		$code .= "  }\r\n";

		$fields="";
		foreach($entity->field as $field){
			$fields .= "  public $".$field->attributes()->name."=null;\r\n";
			__debug(__FILE__,__LINE__,"Model::createClass() - add field: ".$field->attributes()->name. " : ".$field->attributes()->type);
		}
		
		$relations="";
		// Generate relations methods
		foreach($entity->relation as $relation) {
			switch($relation->attributes()->type){
				case "one-to-many":
					__debug(__FILE__,__LINE__,"Model::createClass() - relation:one-to-many from ".$entity->persistence->attributes()->table." to ".$this->entities["".$relation->attributes()->entity]->persistence->attributes()->table);
					$tableKey=(isset($relation->attributes()->relationTable)?$relation->attributes()->relationTable:$this->entities["".$relation->attributes()->entity]->persistence->attributes()->table);
					$relations .="  public function get".ucwords($relation->attributes()->name)."(){\r\n";
					$relations .="    return \$this->retrieveList('".$tableKey."',".
							$this->generateTableKeyArrayToString($relation->attributes()->relationKey).",".
							$this->generatePrimaryKeyArrayToString($entity->attributes()->name).");\r\n";
					break;
				case "many-to-one":
					__debug(__FILE__,__LINE__,"Model::createClass() - add field: ".$relation->attributes()->name);
					$fields .= "  public $".$relation->attributes()->name."=null;\r\n";
					__debug(__FILE__,__LINE__,"Model::createClass() - relation:many-to-one from ".$entity->persistence->attributes()->table." to ".$this->entities["".$relation->attributes()->entity]->persistence->attributes()->table);
					$tableKey=(isset($relation->attributes()->relationTable)?$relation->attributes()->relationTable:$this->entities["".$relation->attributes()->entity]->persistence->attributes()->table);
					$tableTarget=$this->entities["".$relation->attributes()->entity];//->attributes()->primarykey;
					__debug(__FILE__,__LINE__,"Model::createClass() - relation:many-to-one target Table=".$tableTarget->attributes()->primaryKey);
					
					$relations .="  public function get".ucwords($relation->attributes()->name)."(){\r\n";
					$relations .="    return \$this->retrieve('".$tableKey."',".
							$this->generateTableKeyArrayToString($relation->attributes()->relationKey).",".
							$this->generatePrimaryKeyArrayToString($relation->attributes()->name).");\r\n";
					break;			
				case "many-to-many":
					__debug(__FILE__,__LINE__,"Model::createClass() - add field: ".$relation->attributes()->relationKey);
					$fields .= "  public $".$relation->attributes()->name."=null;\r\n";
					__debug(__FILE__,__LINE__,"Model::createClass() - relation:many-to-many from ".$entity->persistence->attributes()->table." to ".$relation->attributes()->relationTable." through ".$this->entities["".$relation->attributes()->entity]->persistence->attributes()->table);
					$tableKey=(isset($relation->attributes()->relationTable)?$relation->attributes()->relationTable:$this->entities["".$relation->attributes()->entity]->persistence->attributes()->table);
					$keyId=$this->generatePrimaryKeyArrayToString($relation->attributes()->relationKey);
					$relations .="  public function get".ucwords($relation->attributes()->name)."(){\r\n";
					$relations .="    return \$this->retrieveListFrom('".$tableKey."',".
							$this->generateTableKeyArrayToString($relation->attributes()->relationKey).",".
							$keyId.");\r\n";
					break;
			}
			$relations .="  }\r\n";			
		}
		$code .=$fields;
		$code .=$relations;
		
		//Generate fields definitions
		__debug(__FILE__,__LINE__,"Model::createClass() - add inherited methods from Abstract Entity class"); 
		$code .="  public function prepareUpdate(){";
		$code .="  }\r\n";			
		$code .="  public function prepareCreate(){";
		$code .="  }\r\n";			
		// end of class.
		$code .= "}\r\n?>";
		__debug(__FILE__,__LINE__,"Model::createClass() - end");
		return $code;
	}
	private function generateTableKeyArrayToString($tableKey){
		return $this->generateKeyToString(
					$tableKey,
					"'{0}'=>\"{0}\"");
	}
	
	//$this->generatePrimaryKeyArrayToString(
	private function generatePrimaryKeyArrayToString($primaryKeys){
		return $this->generateKeyToString(
					$primaryKeys,
					"'{0}'=>\$this->{0}");
	}
	private function generateKeyToString($values,$format){
		$keyIds=split(",",$values);
		$tableIds = array();
		foreach($keyIds as $key){
			$tableIds[$key]=str_replace("{0}",$key,$format);
		}
		$keyId="array(".implode(",",$tableIds).")";
		return $keyId;
	}
	/**
	 * generate code fo the <code>$entity</code>
	 * @param array $entity
	 * @param string $code
	 */
	public function serialize($entity,$code){
		__debug(__FILE__,__LINE__,"Model::serialize() - Write ".$entity->attributes()->name." Class to file ");
		$file=fopen(dirname($_SERVER['SCRIPT_FILENAME'])."/app/entities/".$entity->attributes()->name.".class.php","w+");
		fwrite($file,$code);
		fclose($file);
	}

	/**
	 * generate code fo the <code>$entity</code>
	 * @param string $entityName
	 * @param string $code
	 * @return filename of the generated script.
	 */
	public function writeSQL($entityName,$sql){
		__debug(__FILE__,__LINE__,"Model::writeSQL() - Write ".$entityName." SQL script ");
		$script=dirname($_SERVER['SCRIPT_FILENAME'])."/app/script/migrate-".$entityName.".sql";
		$file=fopen($script,"w+");
		fwrite($file,$sql);
		fclose($file);
		return $script;
	}
	/**
	 * Display the debug generator panel.
	 */
	public function displayDebugPanel(){
		echo "<span class=\"subpanel\"><span class=\"title\">Generator</span>".
			 "<a href=\"?generate=true\" title=\"generate class and table if not already exists\" accesskey=\"G\">Generate</a>".
			 "<a href=\"?generate=true&force=true\" title=\"generate all model (class&tables)\" accesskey=\"F\">Force</a>".
			 "<a href=\"?reload=true\" title=\"Load data from xml files\" accesskey=\"L\">load XML</a>".
			 "</span>";
	}
	
	/**
	 * return the conneciton object.
	 * @return Connection object to the configured database (see Config object for more details). 
	 */
	public function getConnection(){
		return $this->connection;
	}
	/**
	 * return entities attribute.
	 * @return array() array containing list of entities managed by the Model object. 
	 */
	public function getEntities(){
		return $this->entities;
	}
	/**
	 * return xml model.
	 * @return SimpleXmlElement XML schema of the data model loaded. 
	 */
	public function getXml(){
		return $this->xml;
	}	
	/**
	 * return the static instance of the Model Manager.
	 * create the instance if does not already exist.
	 */
	function getInstance($conn){
		__debug(__FILE__,__LINE__,"Model::getInstance() - return instance");
		
		if(!isset(self::$_instance)){
			self::$_instance = new Model($conn);
			self::$_instance->init();
		}
		return self::$_instance;
	}
}
function __GENERATE_DEBUG(){
	Model::displayDebugPanel();
}
?>